// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "shill/vpn_service.h"

#include <algorithm>

#include <base/strings/stringprintf.h>
#include <chromeos/dbus/service_constants.h>

#include "shill/key_value_store.h"
#include "shill/logging.h"
#include "shill/manager.h"
#include "shill/profile.h"
#include "shill/property_accessor.h"
#include "shill/technology.h"
#include "shill/vpn_driver.h"
#include "shill/vpn_provider.h"

using base::Bind;
using base::StringPrintf;
using base::Unretained;
using std::replace_if;
using std::string;

namespace shill {

const char VPNService::kAutoConnNeverConnected[] = "never connected";
const char VPNService::kAutoConnVPNAlreadyActive[] = "vpn already active";

VPNService::VPNService(ControlInterface *control,
                       EventDispatcher *dispatcher,
                       Metrics *metrics,
                       Manager *manager,
                       VPNDriver *driver)
    : Service(control, dispatcher, metrics, manager, Technology::kVPN),
      driver_(driver) {
  SetConnectable(true);
  set_save_credentials(false);
  mutable_store()->RegisterString(kVPNDomainProperty, &vpn_domain_);
  mutable_store()->RegisterDerivedString(
          kPhysicalTechnologyProperty,
          StringAccessor(
              new CustomAccessor<VPNService, string>(
                  this,
                  &VPNService::GetPhysicalTechologyProperty,
                  NULL)));
}

VPNService::~VPNService() {}

void VPNService::Connect(Error *error, const char *reason) {
  if (IsConnected()) {
    Error::PopulateAndLog(error, Error::kAlreadyConnected,
                          StringPrintf("VPN service %s already connected.",
                                       unique_name().c_str()));
    return;
  }
  if (IsConnecting()) {
    Error::PopulateAndLog(error, Error::kInProgress,
                          StringPrintf("VPN service %s already connecting.",
                                       unique_name().c_str()));
    return;
  }
  Service::Connect(error, reason);
  driver_->Connect(this, error);
}

void VPNService::Disconnect(Error *error) {
  LOG(INFO) << "Disconnect from service " << unique_name();
  Service::Disconnect(error);
  driver_->Disconnect();
}

string VPNService::GetStorageIdentifier() const {
  return storage_id_;
}

// static
string VPNService::CreateStorageIdentifier(const KeyValueStore &args,
                                           Error *error) {
  string host = args.LookupString(kProviderHostProperty, "");
  if (host.empty()) {
    Error::PopulateAndLog(
        error, Error::kInvalidProperty, "Missing VPN host.");
    return "";
  }
  string name = args.LookupString(kNameProperty, "");
  if (name.empty()) {
    Error::PopulateAndLog(error, Error::kNotSupported, "Missing VPN name.");
    return "";
  }
  string id = StringPrintf("vpn_%s_%s", host.c_str(), name.c_str());
  replace_if(id.begin(), id.end(), &Service::IllegalChar, '_');
  return id;
}

string VPNService::GetDeviceRpcId(Error *error) const {
  error->Populate(Error::kNotSupported);
  return "/";
}

bool VPNService::Load(StoreInterface *storage) {
  return Service::Load(storage) &&
      driver_->Load(storage, GetStorageIdentifier());
}

bool VPNService::Save(StoreInterface *storage) {
  return Service::Save(storage) &&
      driver_->Save(storage, GetStorageIdentifier(), save_credentials());
}

bool VPNService::Unload() {
  // The base method also disconnects the service.
  Service::Unload();

  set_save_credentials(false);
  driver_->UnloadCredentials();

  // Ask the VPN provider to remove us from its list.
  manager()->vpn_provider()->RemoveService(this);

  return true;
}

void VPNService::InitDriverPropertyStore() {
  driver_->InitPropertyStore(mutable_store());
}

void VPNService::EnableAndRetainAutoConnect() {
  // The base EnableAndRetainAutoConnect method also sets auto_connect_ to true
  // which is not desirable for VPN services.
  RetainAutoConnect();
}

void VPNService::SetConnection(const ConnectionRefPtr &connection) {
  // Construct the connection binder here rather than in the constructor because
  // there's really no reason to construct a binder if we never connect to this
  // service. It's safe to use an unretained callback to driver's method because
  // both the binder and the driver will be destroyed when this service is
  // destructed.
  if (!connection_binder_.get()) {
    connection_binder_.reset(
        new Connection::Binder(unique_name(),
                               Bind(&VPNDriver::OnConnectionDisconnected,
                                    Unretained(driver_.get()))));
  }
  // Note that |connection_| is a reference-counted pointer and is always set
  // through this method. This means that the connection binder will not be
  // notified when the connection is destructed (because we will unbind it first
  // here when it's set to NULL, or because the binder will already be destroyed
  // by ~VPNService) -- it will be notified only if the connection disconnects
  // (e.g., because an underlying connection is destructed).
  connection_binder_->Attach(connection);
  Service::SetConnection(connection);
}

bool VPNService::IsAutoConnectable(const char **reason) const {
  if (!Service::IsAutoConnectable(reason)) {
    return false;
  }
  // Don't auto-connect VPN services that have never connected. This improves
  // the chances that the VPN service is connectable and avoids dialog popups.
  if (!has_ever_connected()) {
    *reason = kAutoConnNeverConnected;
    return false;
  }
  // Don't auto-connect a VPN service if another VPN service is already active.
  if (manager()->vpn_provider()->HasActiveService()) {
    *reason = kAutoConnVPNAlreadyActive;
    return false;
  }
  return true;
}

string VPNService::GetTethering(Error *error) const {
  ConnectionRefPtr conn = connection();
  if (conn)
    conn = conn->GetCarrierConnection();

  string tethering;
  if (conn) {
    tethering = conn->tethering();
    if (!tethering.empty()) {
      return tethering;
    }
    // The underlying service may not have a Tethering property.  This is
    // not strictly an error, so we don't print an error message.  Populating
    // an error here just serves to propagate the lack of a property in
    // GetProperties().
    error->Populate(Error::kNotSupported);
  } else {
    Error::PopulateAndLog(error,
                          Error::kOperationFailed,
                          Error::GetDefaultMessage(Error::kOperationFailed));
  }
  return "";
}

bool VPNService::SetNameProperty(const string &name, Error *error) {
  if (name == friendly_name()) {
    return false;
  }
  LOG(INFO) << "Renaming service " << unique_name() << ": "
            << friendly_name() << " -> " << name;

  KeyValueStore *args = driver_->args();
  args->SetString(kNameProperty, name);
  string new_storage_id = CreateStorageIdentifier(*args, error);
  if (new_storage_id.empty()) {
    return false;
  }
  string old_storage_id = storage_id_;
  DCHECK_NE(old_storage_id, new_storage_id);

  SetFriendlyName(name);

  // Update the storage identifier before invoking DeleteEntry to prevent it
  // from unloading this service.
  storage_id_ = new_storage_id;
  profile()->DeleteEntry(old_storage_id, NULL);
  profile()->UpdateService(this);
  return true;
}

string VPNService::GetPhysicalTechologyProperty(Error *error) {
  ConnectionRefPtr conn = connection();
  if (conn)
    conn = conn->GetCarrierConnection();

  if (!conn) {
    Error::PopulateAndLog(error,
                          Error::kOperationFailed,
                          Error::GetDefaultMessage(Error::kOperationFailed));
    return "";
  }

  return Technology::NameFromIdentifier(conn->technology());
}

}  // namespace shill
